/*
 * Copyright (c) Huawei Technologies Co., Ltd. 2024-2024. All rights reserved.
 *
 */

import { exec } from 'child_process';
import { createHash } from 'crypto';
import { createReadStream, Stats } from 'fs';
import path from 'path';
import { pipeline } from 'stream';

import { hvigorCore } from '@ohos/hvigor';
import * as fs from 'fs-extra';
import { minimatch } from 'minimatch';

import { NativeLibraryModel } from '../../model/cxx/native-library-model.js';
import { NapiLibFilterOpt, PkgSelection } from '../../options/build/build-opt.js';
import { Dependency } from '../../project/dependency/core/dependency-interface.js';
import { FileUtil } from '../../utils/file-util.js';
import { OhosLogger } from '../../utils/log/ohos-logger.js';

const logger: OhosLogger = OhosLogger.getLogger('NativeStrip');
const whiteListLibs = ['libc++_shared.so'];

export interface ProcessLibsOptions {
  outputDir: string;
  filterRule?: NapiLibFilterOpt;
  allHarLibs: LibWithPkgInfo[];
  localLibs: LibWithPkgInfo[];
  outputLibs: LibWithPkgInfo[];
  profilePath: string;
  moduleName: string;
  cangjieLibs: LibWithPkgInfo[];
  collectAllLibs: boolean | undefined;
  readElf?: string;
  localOutputDir?: string;
  compileCommandsPath: string[] | undefined;
  isHarType: boolean | undefined;
  nativeOptionArg?: string | string[] | undefined;
  nativeLibs?: Map<string, NativeLibraryModel>[] | undefined;
}

export const buildLibs = (libsInfo: string[], pkg: Dependency, cwd: string) => {
  return libsInfo
    .map((resolvedPath) => {
      return {
        name: path.basename(resolvedPath),
        path: path.resolve(cwd, resolvedPath),
        relativePath: resolvedPath,
        pkgName: pkg.getPackageName(),
        pkgVersion: pkg.getDependencyVersion(),
        isLocal: pkg.isLocal(),
        selected: 0,
      } as LibWithPkgInfo;
    })
    .filter((file) => !fs.lstatSync(file.path).isDirectory());
};

export const filterBySelection = (collections: LibWithPkgInfo[], select: PkgSelection[]) => {
  if (select.length === 0) {
    return collections;
  }
  select.forEach((rule) => applyForSelectionRule(collections, rule));
  return collections;
};

export const applyForSelectionRule = (collections: LibWithPkgInfo[], rule: PkgSelection) => {
  const matchPkgLibs = collections.filter((lib) => lib.pkgName === rule.package).filter((lib) => !rule.version || lib.pkgVersion === rule.version);
  const includeLibs = matchPkgLibs.filter((lib) => rule.include.includes(lib.name));
  includeLibs.forEach((lib) => (lib.selected = 1));
  let matchedNames;
  if (includeLibs.length > 0) {
    matchedNames = includeLibs.map((lib) => lib.name).join(', ');
    logger.debug(`Selection rule :${rule.package}:${rule.version ?? ''}:include hit for ${matchedNames}`);
  }
  const excludeLibs = matchPkgLibs.filter((lib) => rule.exclude?.includes(lib.name));
  excludeLibs.forEach((lib) => (lib.selected = 2));
  if (excludeLibs.length > 0) {
    matchedNames = excludeLibs.map((lib) => lib.name).join(', ');
    logger.debug(`Selection rule:${rule.package}:${rule.version ?? ''}:exclude hit for ${matchedNames}`);
  }
};

export async function processLibs(config: ProcessLibsOptions) {
  const collections = [...config.allHarLibs.reverse(), ...config.outputLibs, ...config.cangjieLibs, ...config.localLibs];
  filterBySelection(collections, config.filterRule?.select ?? []);
  const { excludes, pickFirsts, pickLasts, enableOverride } = {
    excludes: [],
    pickFirsts: [],
    pickLasts: [],
    enableOverride: false,
    ...config.filterRule,
  };

  // output路径
  fs.emptyDirSync(config.outputDir);

  // 先收集所有的目标.so文件，存进set内，在收集时进行校验
  const fullSet = new Map<string, string[]>();
  const selected: string[] = [];
  collections.forEach((lib) => {
    if (lib.selected === 2 || selected.includes(lib.relativePath)) {
      return;
    }
    if (lib.selected === 1) {
      fullSet.set(lib.relativePath, [lib.path]);
      selected.push(lib.relativePath);
      return;
    }

    // 应用excludes规则
    if (excludes.some((pattern) => minimatch(lib.relativePath, pattern))) {
      logger.debug(`The filter condition excludes matched ${lib.path}.`);
      return;
    }

    // 应用pickLasts规则，选择优先度更靠后的，即更早被找到的.so文件
    if (pickLasts.some((pattern) => minimatch(lib.relativePath, pattern)) && fullSet.has(lib.relativePath)) {
      logger.debug(`The filter condition pickLasts matched ${lib.path}.`);
      return;
    }

    // 应用pickFirsts和enableOverride规则，选择优先度更高的，即更晚被找到的.so文件
    if (pickFirsts.some((pattern) => minimatch(lib.relativePath, pattern)) || enableOverride) {
      fullSet.set(lib.relativePath, [lib.path]);
      logger.debug(`The filter condition pickFirsts or enableOverride matched ${lib.path}.`);
      return;
    }

    if (!fullSet.has(lib.relativePath)) {
      fullSet.set(lib.relativePath, [lib.path]);
      return;
    }

    // Safety：上面已判断fullSet.has(soPath)是否为true
    fullSet.get(lib.relativePath)!.push(lib.path);
  });

  await checkDuplicatedNativeLibs(fullSet, config.profilePath, config.moduleName);

  for (const [key, value] of fullSet) {
    // Safety：fullSet每次set都不为空
    await FileUtil.linkFile(value.pop()!, path.resolve(config.outputDir, key));
  }

  if (!config.isHarType && hvigorCore.getExtraConfig().get('ohos-enable-hwasan')) {
    await checkCompiledLibs(config);
    await checkPrecompileLibs(config);
  }
}

async function checkPrecompileLibs(config: ProcessLibsOptions) {
  const localLibs = getNativeLibs(config);
  const allLibs = await getSubFolderLibs(config.outputDir);
  const depLibs = compareLibraryObjects(allLibs, localLibs);
  const noHwasanLibs = await getNoHwasanLibs(depLibs, config.readElf);

  noHwasanLibs.length && logHwasanWarning(noHwasanLibs);
}

function getNativeLibs(config: ProcessLibsOptions) {
  const nativeLibsObj: { [key: string]: string[] } = {};
  config.nativeLibs?.forEach((nativeLib) => {
    nativeLib.forEach((libraryModel) => {
      const abi = libraryModel.getAbi();
      const nameOnDisk = libraryModel.getNameOnDisk();
      if (config.localOutputDir && abi) {
        const libPath = path.resolve(config.localOutputDir, abi);
        if (libPath && nameOnDisk) {
          if (nativeLibsObj[libPath]) {
            nativeLibsObj[libPath].push(nameOnDisk);
          } else {
            nativeLibsObj[libPath] = [nameOnDisk];
          }
        }
      }
    });
  });

  return nativeLibsObj;
}

async function checkCompiledLibs(config: ProcessLibsOptions) {
  if (!config.compileCommandsPath?.length) {
    return;
  }

  for (const commandPath of config.compileCommandsPath) {
    try {
      const data = await fs.promises.readFile(path.resolve(commandPath), 'utf8');
      const matchCommandField = JSON.parse(data).filter(
        (obj: { command: string | string[] }) => obj.command && typeof obj.command === 'string' && !obj.command.includes('fsanitize=hwaddress'),
      );
      const matchCommandOutput = matchCommandField.map((obj: { file: string }) => obj.file);
      if (matchCommandOutput.length) {
        logHwasanWarning(matchCommandOutput);
      }
    } catch (err) {
      logger.debug(err);
    }
  }
}

function logHwasanWarning(libs: string[]) {
  logger.warn(`HWASAN is not enabled for the following products: \n  ${libs.join('\n  ')}
  Check the compilation script and ensure that the HWASAN of all native products is enabled.`);
}

async function getNoHwasanLibs(libs: { [key: string]: string[] }, readElf: string | undefined): Promise<string[]> {
  const noHwasanLibs: string[] = [];

  for (const libPath in libs) {
    if (Object.prototype.hasOwnProperty.call(libs, libPath)) {
      for (const lib of libs[libPath]) {
        try {
          const isStatic = lib.includes('.a');
          const command = isStatic ? '-s' : '-d';
          const searchParam = isStatic ? '__hwasan_init' : 'libclang_rt.hwasan.so';
          const { stdout } = await execPromise(`"${readElf}" ${command} "${libPath}${path.sep}${lib}"`);

          !stdout.includes(searchParam) && noHwasanLibs.push(`${libPath}${path.sep}${lib}`);
        } catch (error) {
          logger.warn(`Error executing HwasanLibs check for ${lib}: ${error}`);
        }
      }
    }
  }

  return noHwasanLibs;
}

function execPromise(command: string): Promise<{ stdout: string }> {
  return new Promise((resolve, reject) => {
    exec(command, (error: Error | null, stdout: string, stderr: string) => {
      if (error) {
        logger.debug(`exec error: ${error.message}`);
        reject(new Error(`exec error: ${error.message}`));
        return;
      }
      if (stderr) {
        logger.debug(`stderr: ${stderr}`);
        reject(new Error(`stderr: ${stderr}`));
        return;
      }
      resolve({ stdout });
    });
  });
}

function compareLibraryObjects(firstObject: { [key: string]: string[] }, secondObject: { [key: string]: string[] }): { [key: string]: string[] } {
  const result: { [key: string]: string[] } = {};

  for (const firstPath in firstObject) {
    if (Object.prototype.hasOwnProperty.call(firstObject, firstPath)) {
      const firstLibs = firstObject[firstPath];

      const secondPath = Object.keys(secondObject).find((path) => getLastPathSegment(firstPath) === getLastPathSegment(path));

      if (secondPath) {
        const secondLibs = secondObject[secondPath];
        const missingLibs = firstLibs.filter((lib) => !secondLibs.includes(lib));

        if (missingLibs.length > 0) {
          result[firstPath] = missingLibs.filter(lib => !whiteListLibs.includes(lib));
        }
      } else {
        result[firstPath] = firstLibs.filter(lib => !whiteListLibs.includes(lib));
      }
    }
  }

  return result;
}

function getLastPathSegment(filePath: string): string {
  const segments = filePath.split(path.sep);
  return segments[segments.length - 1];
}

async function getSubFolderLibs(outputDir: string | undefined): Promise<{ [key: string]: string[] }> {
  if (!outputDir) {
    return {};
  }

  try {
    const subFolders = await fs.promises.readdir(outputDir);
    const libsObject: { [key: string]: string[] } = {};

    await Promise.all(
      subFolders.map(async (subFolder) => {
        const libPath = path.join(outputDir, subFolder);
        libsObject[libPath] = await fs.promises.readdir(libPath);
      }),
    );

    return libsObject;
  } catch (error) {
    logger.debug('Error reading output directory:', error);
    return {};
  }
}

async function checkDuplicatedNativeLibs(fullSet: Map<string, string[]>, profilePath: string, moduleName: string) {
  const duplicated = [];
  for (const key of fullSet.keys()) {
    const val = fullSet.get(key);
    const fileName = path.basename(key);
    if (val && val.length > 1 && !(fileName === 'libc++.so' || fileName === 'libc++_shared.so')) {
      const deDuplicated = await removeDuplicateFiles(val);
      if (deDuplicated.length < 2) {
        continue;
      }
      duplicated.push(...deDuplicated);
    }
  }
  if (duplicated.length) {
    const errorMessage = duplicated.reduce((pre, cur) => `${pre}- ${cur}\n`, '');
    logger.printErrorExit('DUPLICATED_FILES_DOUND_IN_LIBPATH', [moduleName, errorMessage.trimEnd()]);
  }
}

/**
 * 去除重复的so文件
 * 根据文件大小, 真实路径, md5等判断是否一致
 *
 * @param files
 * @private
 */
async function removeDuplicateFiles(files: string[]) {
  if (files.length < 2) {
    return files;
  }
  const unique: FileIdentifier[] = [];
  for (const file of files) {
    await collectUniqueFile(file, unique);
  }
  return unique.map((file) => file.file);
}

async function collectUniqueFile(file: string, seen: FileIdentifier[]) {
  const stats = fs.lstatSync(file);
  const realPath = stats.isSymbolicLink() ? await fs.readlink(file) : file;
  if (seen.length === 0) {
    seen.push({ file: realPath, stats });
    return;
  }
  for (const other of seen) {
    const so = `${file} | ${other.file}`;
    if (!fs.existsSync(realPath)) {
      logger.debug(`Invalid file or symlink '${file}'.`);
      continue;
    }
    if (realPath === other.file) {
      logger.debug(`SymbolicLink relation between ${so}.`);
      continue;
    }
    if (stats.size !== other.stats.size) {
      logger.debug(`Different file size for ${so}.`);
      seen.push({ file: realPath, stats });
      continue;
    }
    const fileHashText = await computeFileHash(realPath);
    other.hash = other.hash ?? (await computeFileHash(other.file));
    if (fileHashText !== other.hash) {
      logger.debug(`Different checksum for ${so}.`);
      seen.push({ file: realPath, stats, hash: fileHashText });
      continue;
    }
    logger.debug(`Same checksum for ${so}.`);
  }
}

async function computeFileHash(file: string) {
  const hash = createHash('MD5');
  return new Promise<string>((resolve) => pipeline(createReadStream(file), hash, () => resolve(hash.digest('hex'))));
}

type FileIdentifier = {
  file: string;
  hash?: string;
  stats: Stats;
};

export interface LibWithPkgInfo {
  name: string;
  path: string;
  relativePath: string;
  pkgName: string;
  pkgVersion: string;
  isLocal: boolean;
  selected: number;
}
